Skip to content

fix(resolver): skip unreadable cwd ancestors - #31198

Closed
chocolatecake777 wants to merge 2 commits into
oven-sh:mainfrom
chocolatecake777:paid-oss/fix-termux-cwd-ancestors
Closed

chocolatecake777 wants to merge 2 commits into
oven-sh:mainfrom
chocolatecake777:paid-oss/fix-termux-cwd-ancestors

Conversation

@chocolatecake777

Copy link
Copy Markdown

Summary

  • skip unreadable cwd ancestors while resolving package metadata so Bun can still run inside a readable child directory
  • treat skipped ancestors as package-boundary resets so metadata from higher readable parents does not leak into the target directory
  • add focused resolver and bun run tests that cover unreadable-ancestor behavior on Linux, plus the small debug-build include fix needed for validation

Issue

Fixes #30859

What changed

  • added a permission-denied check in the resolver's directory walk and continue past unreadable non-target ancestors instead of failing cwd resolution
  • reset inherited parent metadata after skipping an unreadable ancestor so package.json and tsconfig lookup stay scoped correctly
  • added resolver coverage for unreadable ancestors and CLI coverage for an isolated Linux environment that reproduces the inaccessible-parent case
  • included <assert.h> in src/jsc/bindings/wtf-bindings.cpp so the patched debug binary builds cleanly for the validation path used here

Verification

  • reproduced the inaccessible-ancestor failure on Linux in containerized tests
  • docker_resolver_unreadable_ancestor_tests: passed (3 passed, 1 skipped) with the patched debug binary
  • docker_landlock_bun_run_tests: passed (2 passed) with the patched debug binary
  • docker_prettier_modified_tests_check: passed
  • Android/Termux native validation was not available in this environment, so this was validated with Linux inaccessible-ancestor and Landlock-style isolated tests instead

Reviewer Notes

  • Risk / compatibility: low; the resolver only changes behavior for permission-denied reads on non-target ancestors and preserves the target-directory error path
  • Follow-up left out intentionally: native Android/Termux confirmation still needs maintainer or reporter validation

Notes

Prepared with AI assistance and manually reviewed/tested.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds graceful handling of permission-denied errors during module resolution directory traversal. When intermediate ancestor directories are inaccessible, the resolver now skips them and continues walking rather than aborting, allowing bun run and module resolution to succeed. The change includes a permission error classifier, traversal logic refactoring, and validation tests under Landlock and EACCES scenarios.

Changes

Graceful directory read error handling during module resolution

Layer / File(s) Summary
Permission-denied error classification helper
src/resolver/resolver.rs
Introduces is_permission_denied_dir_read_error predicate to classify directory read errors as permission/access-denied equivalents.
Directory traversal refactoring with graceful permission error handling
src/resolver/resolver.rs
Refactors queue-based directory traversal to track parent_result separately, compute is_target_dir from queue_slice_len, and handle permission-denied errors on non-target directories by assigning a missing_parent_result and continuing the walk instead of aborting resolution.
CLI tests for Landlock-restricted directory access
test/cli/run/run_command.test.ts
Expands imports with filesystem, Linux, and Landlock/FFI helpers; generates an FFI-based Landlock helper script; detects Landlock availability; and validates that bun run succeeds with inaccessible ancestors and fails appropriately when the target directory is inaccessible.
Module resolution regression tests for EACCES
test/js/bun/resolve/resolve.test.ts
Adds three EACCES-triggered test cases verifying module resolution succeeds when ancestors are unreadable, fails when ancestors block imports metadata inheritance, and fails when the working directory itself is unreadable.

Suggested reviewers

  • RiskyMH
  • dylan-conway
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(resolver): skip unreadable cwd ancestors' directly and clearly describes the main change: resolver now handles unreadable current-working-directory ancestors gracefully.
Description check ✅ Passed The description covers the required template sections with comprehensive details on what changed, how it was verified, and includes issue reference, but the template sections are implicit rather than explicitly titled.
Linked Issues check ✅ Passed The PR directly addresses issue #30859 by implementing logic to skip unreadable cwd ancestors, enabling commands like 'bun run' to succeed when target directories are readable despite inaccessible parents.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the issue: resolver skips unreadable ancestors, tests validate the behavior, and assert.h inclusion enables the debug build validation path.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch paid-oss/fix-termux-cwd-ancestors

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/cli/run/run_command.test.ts`:
- Around line 190-213: canUseLandlock() currently only verifies the ruleset
blocks / while allowing allowedDir but doesn't confirm that a real bun process
can start under that restricted allowlist; update the probe so the helper
(written by writeLandlockHelper) actually launches a trivial bun invocation
(e.g., bun --version or a minimal bun script) inside the landlocked environment
and the Bun.spawnSync call checks both LANDLOCK_OK and the bun invocation's
expected output; adjust the helper invocation arguments (helperPath and the
bunExe passed to it) and the spawn/result checks in canUseLandlock() so the
probe only returns true when the helper reports LANDLOCK_OK and the launched bun
command succeeds.

In `@test/js/bun/resolve/resolve.test.ts`:
- Around line 836-862: The test currently only asserts non-empty stderr and
non-zero exit, which can mask unrelated failures; update the assertion after
spawning Bun (variables proc, stdout, stderr, exitCode) to explicitly check that
stderr contains the unreadable-cwd error string (e.g.,
"CouldntReadCurrentDirectory" or the exact platform-specific message your
runtime emits) in addition to keeping the exitCode non-zero and stdout empty so
the test specifically verifies the unreadable-current-directory failure path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b4c8c382-85b5-469d-a8c4-d86db75c3c13

📥 Commits

Reviewing files that changed from the base of the PR and between dcc0434 and 277b8ef.

📒 Files selected for processing (4)
  • src/jsc/bindings/wtf-bindings.cpp
  • src/resolver/resolver.rs
  • test/cli/run/run_command.test.ts
  • test/js/bun/resolve/resolve.test.ts

Comment on lines +190 to +213
function canUseLandlock() {
if (!isLinux) return false;

const root = tempDirWithFiles("run-landlock-probe", {});
try {
const testBase = join(root, "parent", "project");
const helperPath = writeLandlockHelper(root);

mkdirSync(testBase, { recursive: true });
const check = Bun.spawnSync({
cmd: [bunExe(), helperPath, testBase, dirname(bunExe()), "--self-check"],
env: bunEnv,
cwd: testBase,
stdout: "pipe",
stderr: "pipe",
});

return check.exitCode === 0 && check.stdout.toString().includes("LANDLOCK_OK");
} finally {
try {
rmSync(root, { recursive: true, force: true });
} catch {}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the Landlock probe validate a real bun launch.

canUseLandlock() only checks that the ruleset can block / while still opening allowedDir. It never proves that the restricted allowlist is sufficient to execute bun, so this can return true and enable the suite even when the sandbox later fails for unrelated loader/libc path reasons. Probe with a trivial bun invocation under the helper so the skip condition matches the actual test preconditions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cli/run/run_command.test.ts` around lines 190 - 213, canUseLandlock()
currently only verifies the ruleset blocks / while allowing allowedDir but
doesn't confirm that a real bun process can start under that restricted
allowlist; update the probe so the helper (written by writeLandlockHelper)
actually launches a trivial bun invocation (e.g., bun --version or a minimal bun
script) inside the landlocked environment and the Bun.spawnSync call checks both
LANDLOCK_OK and the bun invocation's expected output; adjust the helper
invocation arguments (helperPath and the bunExe passed to it) and the
spawn/result checks in canUseLandlock() so the probe only returns true when the
helper reports LANDLOCK_OK and the launched bun command succeeds.

Comment on lines +836 to +862
it.skipIf(!canTriggerEACCES)("module resolution still fails when cwd itself is unreadable", async () => {
using dir = tempDir("resolver-inaccessible-target", {
"project/entry.js": `console.log("should not run");\n`,
});
const root = String(dir);
const project = join(root, "project");

if (canUseRunuser) {
chmodSync(root, 0o755);
chmodSync(project, 0o755);
chmodSync(join(project, "entry.js"), 0o644);
}

try {
chmodSync(project, 0o111);
await using proc = Bun.spawn({
cmd: canUseRunuser ? ["runuser", "-u", "nobody", "--", bunExe(), "entry.js"] : [bunExe(), "entry.js"],
env: bunEnv,
cwd: project,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stdout).toBe("");
expect(stderr).not.toBe("");
expect(exitCode).not.toBe(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert the unreadable-cwd error explicitly.

This currently passes on any nonzero failure with any stderr, so the regression can stay green even if the process dies for an unrelated reason before hitting the target-directory path. Match the concrete error for this case (for example CouldntReadCurrentDirectory) so the test actually guards the behavior this PR is preserving.

Suggested tightening
-      expect(stderr).not.toBe("");
+      expect(stderr).toContain("CouldntReadCurrentDirectory");
       expect(exitCode).not.toBe(0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it.skipIf(!canTriggerEACCES)("module resolution still fails when cwd itself is unreadable", async () => {
using dir = tempDir("resolver-inaccessible-target", {
"project/entry.js": `console.log("should not run");\n`,
});
const root = String(dir);
const project = join(root, "project");
if (canUseRunuser) {
chmodSync(root, 0o755);
chmodSync(project, 0o755);
chmodSync(join(project, "entry.js"), 0o644);
}
try {
chmodSync(project, 0o111);
await using proc = Bun.spawn({
cmd: canUseRunuser ? ["runuser", "-u", "nobody", "--", bunExe(), "entry.js"] : [bunExe(), "entry.js"],
env: bunEnv,
cwd: project,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).not.toBe("");
expect(exitCode).not.toBe(0);
it.skipIf(!canTriggerEACCES)("module resolution still fails when cwd itself is unreadable", async () => {
using dir = tempDir("resolver-inaccessible-target", {
"project/entry.js": `console.log("should not run");\n`,
});
const root = String(dir);
const project = join(root, "project");
if (canUseRunuser) {
chmodSync(root, 0o755);
chmodSync(project, 0o755);
chmodSync(join(project, "entry.js"), 0o644);
}
try {
chmodSync(project, 0o111);
await using proc = Bun.spawn({
cmd: canUseRunuser ? ["runuser", "-u", "nobody", "--", bunExe(), "entry.js"] : [bunExe(), "entry.js"],
env: bunEnv,
cwd: project,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toContain("CouldntReadCurrentDirectory");
expect(exitCode).not.toBe(0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/js/bun/resolve/resolve.test.ts` around lines 836 - 862, The test
currently only asserts non-empty stderr and non-zero exit, which can mask
unrelated failures; update the assertion after spawning Bun (variables proc,
stdout, stderr, exitCode) to explicitly check that stderr contains the
unreadable-cwd error string (e.g., "CouldntReadCurrentDirectory" or the exact
platform-specific message your runtime emits) in addition to keeping the
exitCode non-zero and stdout empty so the test specifically verifies the
unreadable-current-directory failure path.

@BenjaBobs

Copy link
Copy Markdown

Also fixes #28220 I think

@Jobians

Jobians commented Jun 5, 2026

Copy link
Copy Markdown

No updates?

Leonisaurov added a commit to Leonisaurov/opencode-termux that referenced this pull request Jul 27, 2026
…/Termux

- Save PR #31198 diff (oven-sh/bun#31198) for reference
- Add resolver.rs and wtf-bindings.cpp hunks to android-support.patch
- Restore build-bun.yml to cross-compilation mode targeting Bun 1.3.14
@robobun

robobun commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Thank you for this contribution!

This was fixed in #31938 and #33119: the resolver now treats EPERM/EACCES on ancestor directories as an empty dir and continues. The relevant code is in src/resolver/resolver.rs on current main.

Closing as already fixed. We really appreciate you taking the time to track this down and submit a patch!

@robobun robobun closed this Jul 31, 2026
@Jobians

Jobians commented Jul 31, 2026

Copy link
Copy Markdown

@robobun please look into #30859

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android aarch64 (Termux): filesystem access + cwd + install permission issues

4 participants